Poznaj zaawansowane funkcje TypeScript, takie jak typy literalne szablonów i typy warunkowe, aby pisać bardziej wyrazisty i łatwiejszy w utrzymaniu kod. Opanuj manipulowanie typami dla złożonych scenariuszy.
Zaawansowane typy TypeScript: Opis opartych na szablonach i typach warunkowych
Siła TypeScript tkwi w jego potężnym systemie typów. Podczas gdy podstawowe typy, takie jak string, number i boolean, są wystarczające dla wielu scenariuszy, zaawansowane funkcje, takie jak typy literalne szablonów i typy warunkowe, otwierają nowy poziom ekspresji i bezpieczeństwa typów. Niniejszy przewodnik zawiera kompleksowy przegląd tych zaawansowanych typów, badając ich możliwości i demonstrując praktyczne zastosowania.
Zrozumienie typów literalnych szablonów
Typy literalne szablonów bazują na szablonach literowych JavaScript, pozwalając na definiowanie typów w oparciu o interpolację ciągów znaków. Umożliwia to tworzenie typów, które reprezentują określone wzorce ciągów znaków, dzięki czemu kod jest bardziej niezawodny i przewidywalny.
Podstawowa składnia i użycie
Typy literalne szablonów używają backticków (`) do otaczania definicji typu, podobnie jak literaly szablonów JavaScript. Wewnątrz backticków można interpolować inne typy za pomocą składni ${}. Właśnie tu dzieje się magia – zasadniczo tworzysz typ, który jest ciągiem znaków, skonstruowanym w czasie kompilacji na podstawie typów wewnątrz interpolacji.
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type APIEndpoint = `/api/${string}`;
// Example Usage
const getEndpoint: APIEndpoint = "/api/users"; // Valid
const postEndpoint: APIEndpoint = "/api/products/123"; // Valid
const invalidEndpoint: APIEndpoint = "/admin/settings"; // TypeScript will not show an error here as `string` can be anything
W tym przykładzie APIEndpoint jest typem, który reprezentuje dowolny ciąg znaków zaczynający się od /api/. Chociaż ten podstawowy przykład jest przydatny, prawdziwa moc typów literalnych szablonów pojawia się w połączeniu z bardziej konkretnymi ograniczeniami typów.
Łączenie z typami unijnymi
Typy literalne szablonów naprawdę błyszczą w połączeniu z typami unijnymi. Umożliwia to tworzenie typów, które reprezentują określony zestaw kombinacji ciągów znaków.
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type APIPath = "users" | "products" | "orders";
type APIEndpoint = `/${APIPath}/${HTTPMethod}`;
// Valid API Endpoints
const getUsers: APIEndpoint = "/users/GET";
const postProducts: APIEndpoint = "/products/POST";
// Invalid API Endpoints (will result in TypeScript errors)
// const invalidEndpoint: APIEndpoint = "/users/PATCH"; // Error: "/users/PATCH" is not assignable to type "/users/GET" | "/users/POST" | "/users/PUT" | "/users/DELETE" | "/products/GET" | "/products/POST" | ... 3 more ... | "/orders/DELETE".
Teraz APIEndpoint jest bardziej restrykcyjnym typem, który zezwala tylko na określone kombinacje ścieżek API i metod HTTP. TypeScript oznaczy wszelkie próby użycia nieprawidłowych kombinacji, zwiększając bezpieczeństwo typów.
Manipulacja ciągami znaków z typami literalnymi szablonów
TypeScript zapewnia wbudowane typy manipulacji ciągami znaków, które działają bezproblemowo z typami literalnymi szablonów. Te typy pozwalają na transformację ciągów znaków w czasie kompilacji.
- Uppercase: Konwertuje ciąg znaków na wielkie litery.
- Lowercase: Konwertuje ciąg znaków na małe litery.
- Capitalize: Ustawia pierwszą literę ciągu znaków na wielką.
- Uncapitalize: Ustawia pierwszą literę ciągu znaków na małą.
type Greeting = "hello world";
type UppercaseGreeting = Uppercase; // "HELLO WORLD"
type LowercaseGreeting = Lowercase; // "hello world"
type CapitalizedGreeting = Capitalize; // "Hello world"
type UncapitalizedGreeting = Uncapitalize; // "hello world"
Te typy manipulacji ciągami znaków są szczególnie przydatne do automatycznego generowania typów na podstawie konwencji nazewnictwa. Na przykład możesz wyprowadzać typy akcji z nazw zdarzeń lub odwrotnie.
Praktyczne zastosowania typów literalnych szablonów
- Definicja punktu końcowego API: Jak pokazano powyżej, definiowanie punktów końcowych API z precyzyjnymi ograniczeniami typów.
- Obsługa zdarzeń: Tworzenie typów dla nazw zdarzeń z określonymi prefiksami i sufiksami.
- Generowanie klas CSS: Generowanie nazw klas CSS na podstawie nazw i stanów komponentów.
- Tworzenie zapytań do bazy danych: Zapewnienie bezpieczeństwa typów podczas tworzenia zapytań do bazy danych.
Międzynarodowy przykład: Formatowanie waluty
Wyobraź sobie budowanie aplikacji finansowej, która obsługuje wiele walut. Możesz użyć typów literalnych szablonów, aby wymusić poprawne formatowanie waluty.
type CurrencyCode = "USD" | "EUR" | "GBP" | "JPY";
type CurrencyFormat = `${number} ${T}`;
const priceUSD: CurrencyFormat<"USD"> = "100 USD"; // Valid
const priceEUR: CurrencyFormat<"EUR"> = "50 EUR"; // Valid
// const priceInvalid: CurrencyFormat<"USD"> = "100 EUR"; // Error: Type 'string' is not assignable to type '`${number} USD`'.
function formatCurrency(amount: number, currency: T): CurrencyFormat {
return `${amount} ${currency}`;
}
const formattedUSD = formatCurrency(250, "USD"); // Type: "250 USD"
const formattedEUR = formatCurrency(100, "EUR"); // Type: "100 EUR"
Ten przykład zapewnia, że wartości walut są zawsze formatowane z poprawnym kodem waluty, zapobiegając potencjalnym błędom.
Zagłębianie się w typy warunkowe
Typy warunkowe wprowadzają logikę rozgałęzień do systemu typów TypeScript, pozwalając na definiowanie typów, które zależą od innych typów. Ta funkcja jest niezwykle potężna do tworzenia wysoce elastycznych i wielokrotnego użytku definicji typów.
Podstawowa składnia i użycie
Typy warunkowe używają słowa kluczowego infer i operatora trójskładnikowego (condition ? trueType : falseType) do definiowania warunków typu.
type IsString = T extends string ? true : false;
type StringCheck = IsString; // type StringCheck = true
type NumberCheck = IsString; // type NumberCheck = false
W tym przykładzie IsString jest typem warunkowym, który sprawdza, czy T jest przypisywalny do string. Jeśli tak, typ jest rozwiązywany do true; w przeciwnym razie jest rozwiązywany do false.
Słowo kluczowe infer
Słowo kluczowe infer pozwala na wyodrębnienie typu z typu. Jest to szczególnie przydatne podczas pracy ze złożonymi typami, takimi jak typy funkcji lub typy tablic.
type ReturnType any> = T extends (...args: any) => infer R ? R : any;
function add(a: number, b: number): number {
return a + b;
}
type AddReturnType = ReturnType; // type AddReturnType = number
W tym przykładzie ReturnType wyodrębnia typ zwracany typu funkcji T. Część infer R typu warunkowego wywnioskuje typ zwracany i przypisuje go do zmiennej typu R. Jeśli T nie jest typem funkcji, typ jest rozwiązywany do any.
Dystrybucyjne typy warunkowe
Typy warunkowe stają się dystrybucyjne, gdy sprawdzanym typem jest nagi parametr typu. Oznacza to, że typ warunkowy jest stosowany do każdego elementu typu unijnego osobno.
type ToArray = T extends any ? T[] : never;
type NumberOrStringArray = ToArray; // type NumberOrStringArray = string[] | number[]
W tym przykładzie ToArray konwertuje typ T na typ tablicy. Ponieważ T jest nagim parametrem typu (nie zawiniętym w inny typ), typ warunkowy jest stosowany do number i string osobno, co powoduje unię number[] i string[].
Praktyczne zastosowania typów warunkowych
- Wyodrębnianie typów zwracanych: Jak pokazano powyżej, wyodrębnianie typu zwracanego funkcji.
- Filtrowanie typów z unii: Tworzenie typu, który zawiera tylko określone typy z unii.
- Definiowanie typów funkcji przeciążonych: Tworzenie różnych typów funkcji w oparciu o typy wejściowe.
- Tworzenie strażników typu: Definiowanie funkcji, które zawężają typ zmiennej.
Międzynarodowy przykład: Obsługa różnych formatów daty
Różne regiony świata używają różnych formatów daty. Możesz użyć typów warunkowych do obsługi tych wariantów.
type DateFormat = "YYYY-MM-DD" | "MM/DD/YYYY" | "DD.MM.YYYY";
type ParseDate = T extends "YYYY-MM-DD"
? { year: number; month: number; day: number; format: "YYYY-MM-DD" }
: T extends "MM/DD/YYYY"
? { month: number; day: number; year: number; format: "MM/DD/YYYY" }
: T extends "DD.MM.YYYY"
? { day: number; month: number; year: number; format: "DD.MM.YYYY" }
: never;
function parseDate(dateString: string, format: T): ParseDate {
// (Implementation would handle different date formats)
if (format === "YYYY-MM-DD") {
const [year, month, day] = dateString.split("-").map(Number);
return { year, month, day, format } as ParseDate;
} else if (format === "MM/DD/YYYY") {
const [month, day, year] = dateString.split("/").map(Number);
return { month, day, year, format } as ParseDate;
} else if (format === "DD.MM.YYYY") {
const [day, month, year] = dateString.split(".").map(Number);
return { day, month, year, format } as ParseDate;
} else {
throw new Error("Invalid date format");
}
}
const parsedDateISO = parseDate("2023-10-27", "YYYY-MM-DD"); // Type: { year: number; month: number; day: number; format: "YYYY-MM-DD"; }
const parsedDateUS = parseDate("10/27/2023", "MM/DD/YYYY"); // Type: { month: number; day: number; year: number; format: "MM/DD/YYYY"; }
const parsedDateEU = parseDate("27.10.2023", "DD.MM.YYYY"); // Type: { day: number; month: number; year: number; format: "DD.MM.YYYY"; }
console.log(parsedDateISO.year); // Access the year knowing it will be there
Ten przykład używa typów warunkowych do definiowania różnych funkcji parsowania daty w oparciu o określony format daty. Typ ParseDate zapewnia, że zwrócony obiekt ma poprawne właściwości w oparciu o format.
Łączenie typów literalnych i warunkowych
Prawdziwa moc pojawia się, gdy połączysz typy literalne szablonów i typy warunkowe. Umożliwia to niezwykle wydajne manipulacje typami.
type EventName = `on${Capitalize}`;
type ExtractEventPayload = T extends EventName
? { type: T; payload: any } // Simplified for demonstration
: never;
type ClickEvent = EventName<"click">; // "onClick"
type MouseOverEvent = EventName<"mouseOver">; // "onMouseOver"
//Example function that takes a type
function processEvent(event: T): ExtractEventPayload {
//In a real implementation, we would actually dispatch the event.
console.log(`Processing event ${event}`);
//In a real implementation, the payload would be based on event type.
return { type: event, payload: {} } as ExtractEventPayload;
}
//Note that the return types are very specific:
const clickEvent = processEvent("onClick"); // { type: "onClick"; payload: any; }
const mouseOverEvent = processEvent("onMouseOver"); // { type: "onMouseOver"; payload: any; }
//If you use other strings, you get never:
// const someOtherEvent = processEvent("someOtherEvent"); // Type is `never`
Najlepsze praktyki i uwagi
- Utrzymuj prostotę: Chociaż są potężne, te zaawansowane typy mogą szybko stać się złożone. Dąż do przejrzystości i łatwości w utrzymaniu.
- Dokładnie testuj: Upewnij się, że definicje typów działają zgodnie z oczekiwaniami, pisząc kompleksowe testy jednostkowe.
- Dokumentuj swój kod: Jasno dokumentuj cel i zachowanie zaawansowanych typów, aby poprawić czytelność kodu.
- Rozważ wydajność: Nadmierne użycie zaawansowanych typów może wpłynąć na czas kompilacji. Profiluj swój kod i optymalizuj w razie potrzeby.
Wnioski
Typy literalne szablonów i typy warunkowe to potężne narzędzia w arsenale TypeScript. Opanowując te zaawansowane typy, możesz pisać bardziej wyrazisty, łatwiejszy w utrzymaniu i bezpieczny typowo kod. Funkcje te umożliwiają uchwycenie złożonych relacji między typami, wymuszanie bardziej rygorystycznych ograniczeń i tworzenie wysoce wielokrotnego użytku definicji typów. Wykorzystaj te techniki, aby podnieść swoje umiejętności TypeScript i budować solidne i skalowalne aplikacje dla globalnej publiczności.